Hash files are cached
-
What you'll see
A developer — or more often an AI code-generation tool — writes a hash file (
.hash) that embeds per-visitor data inline using#...#-expressions. Common shapes:<img src="#docly.getProfilePictureUrl(request.user)#" alt="Profile"> <p>Welcome, #request.user#</p> <p>Generated #new Date().toISOString()#</p>Neither the name nor the profile picture shows the visitor:
request.userisnullin a page, for everyone. The time is rendered once, and from then on every visitor gets the first visitor’s cached HTML with that time in it. -
What's actually happening
Docly hash files are display templates that the HashJS engine renders to HTML. The rendered output is cached and served to later visitors without running the template again. Dynamic content belongs in
#/API/, which is never cached.The cache key is the page's path — not the visitor, and not the query string:
/page?id=1and/page?id=2share one cached copy (a template cannot read the query string anyway —queryexists only in API scripts). Everyone who opens the page gets the same HTML. Anything that depends on the visitor — the IP, the user agent, the current time — must therefore not appear inside#...#in a hash file.Publications with login are cached too. On a publication that requires login, the platform checks access first and then serves the cached page — and the cache is shared by all logged-in users. One copy serves them all, so the page cannot know who is viewing it.
A page never sees the signed-in user.
request.userandrequest.Jwtarenullin hash pages, and in a.doclydisplay templateUseris not set — also when the document is pulled in with#include(…)#. They are set only in API functions in#/API/, which are never cached. A check like#if (!request.user) …#in a page is therefore always true.The vulnerability class this prevents is data leakage: if a page could read the visitor, the first visitor after cache invalidation would populate the cache with their own data, and every later visitor would receive it until the cache was rebuilt.
-
What to do
Put per-request logic in
#/API/-functions. From the hash file, use static HTML plus browser JS that calls the API at runtime viafetch(). The hash itself stays static and cacheable; the dynamic data flows through the API on every request.Do — split static hash from dynamic API:
// #/API/profile.js export default () => { if (!request.Jwt) return docly.denyAccess(); return { username: request.Jwt.username, pictureUrl: docly.getProfilePictureUrl(request.Jwt.username) }; }<!-- profile.hash — static HTML, dynamic data fetched at runtime --> <img id="avatar" alt="Profile"> <p>Welcome, <span id="name"></span></p> <script> fetch('~/API/profile') .then(r => r.json()) .then(d => { document.getElementById('avatar').src = d.pictureUrl; document.getElementById('name').innerText = d.username; }); </script>Don’t — per-request expression in hash (neither shows the visitor, since
request.userisnullin a page):<img src="#docly.getProfilePictureUrl(request.user)#" alt="Profile"> <p>Welcome, #request.user#</p>Access control follows the same rule:
docly.denyAccess()only exists in API functions, so never put access checks orper-user checks inside#...#in a hash file. Protect the endpoint behind the data instead — let the API calldenyAccess(), and let the hash page show a “loading…” state until the fetch resolves or fails.
Last reviewed 25 Oct 2026